from CTPRPCCommon import *
from datetime import datetime
import logging, traceback

try:
    from hashlib import md5
except ImportError:
    from md5 import md5

import zlib

default_logger = logging.getLogger('CTPRPC3')
default_logger.setLevel(logging.WARN)
default_logger.addHandler(logging.StreamHandler())

class RemoteException(CTPRPCError):
    """
    Keeps track of an exception raised on a remote side
    of the CTPRPC communication
    """
    def __init__(self, reclass, reargs, remsg, retb):
        super(RemoteException, self).__init__(*reargs)
        self.message = '%s: %s' % (reclass, remsg)
        self.exception_class = reclass
        self.tb = retb

    def __str__(self):
        s = '\n'
        s += "Traceback (most recent call last):\n"
        s += ''.join(traceback.format_list (self.tb))
        s += self.message
        return s

class CTPRPCClientEventsLogger(object):
    def __init__(self, logger=None):
        if not logger:
            logger = default_logger
        self.logger = logger

    def reconnected(self, when):
        self.logger.warning('[%s] Client reconnected due to a timeout', when)

    def got_call_answer(self, when, method, data):
        self.logger.info('[%s] Received answer to method %s call', when, method)

    def sent_call(self, when, method):
        self.logger.info('[%s] Method call request correctly sent for %s', when , method)

    def quit(self, when, method):
        self.logger.info('[%s] IOLayer performed quit on %s', when, method)

    def warning(self, when, what):
        self.logger.warning('[%s] IOLayer reported a warning: %s', when, what)

    def warn_method_call_timeout(self, when, method):
        self.logger.warning('[%s] Method call timeout for "%s"', when, method)
        return False

    def err_timeout(self, when):
        self.logger.error('[%s] Fatal Connection Timeout', when)
        return False

    def err_protocol_io(self, when, data):
        self.logger.error("[%s] IO Layer didn't understand my request or the server response", when)
        return False

    def err_remote_exception(self, when, method, data):
        self.logger.warning('[%s] Remote exception while calling %s (%s)', when, method, data['excp'].message)
        return False

    def err_method_not_found(self, when, method):
        self.logger.warning('[%s] method "%s" not found on remote object', when, method)
        return False

    def err_authentication(self):
        self.logger.error('[%s] Unable to authenticate to the server', datetime.now())
        return False

class CTPRPCServerEventsLogger(object):
    def __init__(self, logger=None):
        if not logger:
            logger = default_logger
        self.logger = logger

    def warn_firing_unregistered_event(self, event_name):
        self.logger.warning('[%s] Firing an unregistered event "%s"', datetime.now(), event_name)

    def accepted(self, who, sid):
        self.logger.debug('[%s] %s, Accepted from %s', datetime.now(), sid, who)

    def fatal_network_error(self):
        self.logger.error('[%s] Fatal network error, terminating server...', datetime.now())

    def fatal_auth_file(self):
        self.logger.warning('[%s] Invalid auth db file, proceding without authentication...', datetime.now())

    def err_malformed_packet(self, sid, e):
        self.logger.error('[%s] %s, Malformed packet caused exception %s', datetime.now(), sid, e)
        return False

    def err_hard_timeout(self, sid):
        self.logger.error('[%s] %s, Hard timeout', datetime.now(), sid)

class CTPRPCPacket(object):
    """A CTPRPC protocol version 3 packet.
    
    You will usually need only to pass the **payload** as the other parameters
    will be filled by the send methods of :class:`os3.net.CTPRPC3Client.CTPRPCClient` and 
    :class:`os3.net.CTPRPC3Server.CTPRPCServer`
    
    **compressed** and **crc** parameters will be automatically filled by the :meth:`os3.net.CTPRPC3Common.CTPRPCPacket.split` method
    """
    MAX_TRANSFER_UNIT = 1400
    CAN_USE_COMPRESSION = True

    def __init__(self, payload, session=0, seq=0, marker=1, crc=None, compressed=0):
        self.session = int(session)
        self.seq = int(seq)
        self.marker = int(marker)
        self.payload = payload
        self.crc = crc or md5(self.payload).hexdigest()
        self.compressed = int(compressed)
        self.tmp_join = [self.payload]

    def dump(self):
        """
        Dumps the packet to a byte array.
        
        Remember to use :meth:`split` instead of :meth:`dump` if you want to be sure that the packet won't be bigger than the MTU
        """
        return '%s-%s-%s-%s-%s-%s' % (self.crc, self.session, self.seq, self.marker, self.compressed, self.payload)

    def generated_split(self):
        def header_size(crc, session, seq):
            return len('%s-%s-%s-%s-%s-' % (crc, session, seq, 1, 0))

        packets = []
        seq = self.seq

        if CTPRPCPacket.CAN_USE_COMPRESSION:
            compressed_payload = zlib.compress(self.payload)
            if len(compressed_payload) >= len(self.payload):
                self.compressed = 0
            else:
                self.payload = compressed_payload
                self.compressed = 1
        else:
            self.compressed = 0

        payload = self.payload
        self.crc = md5(self.payload).hexdigest()

        packet = self
        payload_pos = 0
        payload_size = len(payload)
        packet_size = header_size(self.crc, self.session, seq) + payload_size
        while packet_size > CTPRPCPacket.MAX_TRANSFER_UNIT:
            new_header_size = header_size(self.crc, self.session, seq)
            payload_trunk_size = CTPRPCPacket.MAX_TRANSFER_UNIT - new_header_size
            splitted_payload = payload[payload_pos:payload_pos + payload_trunk_size]
            packet = CTPRPCPacket(splitted_payload, self.session, seq, 0, self.crc, self.compressed)
            yield packet

            seq += 1
            payload_pos += payload_trunk_size
            payload_size -= payload_trunk_size
            packet_size = header_size(self.crc, self.session, seq) + payload_size

        payload = payload[payload_pos:payload_pos + payload_size]
        packet = CTPRPCPacket(payload, self.session, seq, 1, self.crc, self.compressed)
        yield packet

    def split(self):
        """Split the packet in a list of packets to let it stay inside the MTU.
        It will also recalculate the checksum for the packet payload to be sure
        that it will be correct as it can't be recalculated after splitting and
        will perform compression if the packet can be compressed
        
        The size of the MTU is taken from the **CTPRPCPacket.MAX_TRANSFER_UNIT** variable and is **1500 by default**
        """
        return [x for x in self.generated_split()]

    @property
    def calculated_crc(self):
        """Return the calculated crc of the packet."""
        return md5(self.payload).hexdigest()

    @property
    def is_valid(self):
        """Checks if the packet payload is valid."""
        return self.calculated_crc == self.crc

    def decompress(self):
        """
        Decompresses the packet, remember to rebuild it and check that it is valid
        before compressing it.
        
        If the packet was not compressed it won't do anything.
        """
        if self.compressed:
            self.payload = zlib.decompress(self.payload)

    @classmethod
    def load(cls, pkt):
        """
        Load a packet from a packet dump.
        
        If the packet was splitted you need to call :meth:`append` to the first packet
        until the **packet marker** becomes **True** or you can call :meth:`rebuild` on
        a list of packets to obtain the rebuilt packet.
        """
        crc, session, seq, marker, compressed, payload = pkt.split('-', 5)
        p = cls(session=session, seq=seq, marker=marker, payload=payload, crc=crc, compressed=compressed)
        return p

    @classmethod
    def rebuild(cls, pkts):
        """Rebuild a packet from a list of packet dumps"""
        if not pkts:
            return None

        p = cls.load(pkts[0])
        for tp in pkts[1:]:
            p.append(cls.load(tp))

        return p

    def append(self, pkt):
        """Append a packet to the current one rebuilding a bigger packet"""
        if self.marker == 1:
            raise Exception('Trying to append a packet to a complete one')
        if pkt.session != self.session:
            raise Exception('Trying to append a packet with a different session')
        if pkt.seq < self.seq:
            raise Exception('Trying to append a packet preceding this one')

        self.marker = pkt.marker
        self.tmp_join.append(pkt.payload)

        if self.marker:
            self.payload = ''.join(self.tmp_join)
            self.tmp_join = [self.payload]

        return pkt.seq

class HBeatPacket(CTPRPCPacket):
    """
    Represents an HEART BEAT packet.
    
    HBeat packets are send between the server and the client to
    keep the connection live. The **status** parameter will
    be *N* or *C* if the remote side is doing Nothing or currently
    executing a remote method Call
    """
    def __init__(self, status='N'):
        super(HBeatPacket, self).__init__('H' + status)

    @property
    def status(self):
        return self.payload[1]

class SubscribePacket(CTPRPCPacket):
    """
    Represents a SUBSCRIBE EVENT packet.
    
    Subscribe packets are sent from the slient to the server
    when the client wants to be notified on an event
    """
    def __init__(self, event_name):
        super(SubscribePacket, self).__init__('S' + event_name)

    @property
    def event_name(self):
        return self.payload[1:]

class CallPacket(CTPRPCPacket):
    """
    Represents a REMOTE METHOD CALL packet.
    
    Call packets are sent both by the client and the server.
    The client will send them to request execution of a remote method,
    while the server will send them to notify to the client
    that an event happened
    """
    def __init__(self, serializer=None, method_call=None):
        if serializer:
            serializer.serialize(method_call)
            method_call['files'] = {}
            for file in serializer.files:
                method_call['files'][file.name] = file.read()
            result = 'C' + serializer.serialize(method_call)
            serializer.files = []
        else:
            result = ''
        super(CallPacket, self).__init__(result)

    def get_parsed_method(self, serializer):
        return serializer.deserialize(self.payload[1:])

class AckPacket(CTPRPCPacket):
    """
    Represents a REMOTE REQUEST EXECUTION COMPLETED packet
    
    Ack will be sent by the server to the client when it
    completes the execution of the specified call. 
    
    If the client doesn't receive any Ack and the server didn't notify the client
    that it is still executing the method (notification happens by using
    :class:`os3.net.CTPRPC3Common.HBeatPacket` packets) the client will require again the execution
    of the method
    """
    def __init__(self, serializer=None, call_id=None, result=None):
        if serializer:
            serializer.serialize(result)
            files = {}
            for file in serializer.files:
                files[file.name] = file.read()
            result = 'A' + serializer.serialize({'id':call_id, 'res':result, 'files':files})
            serializer.files = []
        else:
            result = ''
        super(AckPacket, self).__init__(result)

    def get_parsed_result(self, serializer):
        return serializer.deserialize(self.payload[1:])

class NotFoundPacket(CTPRPCPacket):
    """
    Represents a REMOTE METHOD NOT FOUND packet
    
    Not Found packets are sent from the server to the client when the client
    requested execution of a method that didn't exist
    """
    def __init__(self, serializer=None, call_id=None):
        if serializer:
            result = 'M' + serializer.serialize({'id':call_id})
        else:
            result = ''
        super(NotFoundPacket, self).__init__(result)

    def get_parsed_result(self, serializer):
        data = serializer.deserialize(self.payload[1:])
        return data

class ExceptionPacket(CTPRPCPacket):
    """
    Represents a REMOTE METHOD EXCEPTION packet
    
    Exception packets are sent from the server to the client when the execution
    of a remote call caused an exception on the server. The exception will be
    repoted by the client as an instance of a :class:`os3.net.CTPRPC3Common.RemoteException`
    """
    def __init__(self, serializer=None, call_id=None, eclass=None, e=None, tb=None):
        if serializer:
            result = serializer.serialize({'id':call_id, 'class':eclass.__name__,
                                           'value':e.args, 'msg':str(e), 'traceback':tb})
        else:
            result = ''
        super(ExceptionPacket, self).__init__('E' + result)

    def get_parsed_result(self, serializer):
        data = serializer.deserialize(self.payload[1:])
        return {'id':data['id'], 'excp':RemoteException(data['class'], data['value'], data['msg'], data['traceback'])}

def parse_packet(pkt):
    if pkt.payload[0] == 'H':
        res = HBeatPacket()
    elif pkt.payload[0] == 'C':
        res = CallPacket()
    elif pkt.payload[0] == 'A':
        res = AckPacket()
    elif pkt.payload[0] == 'E':
        res = ExceptionPacket()
    elif pkt.payload[0] == 'M':
        res = NotFoundPacket()
    elif pkt.payload[0] == 'S':
        res = SubscribePacket(pkt.payload[1:])
    else:
        res = None

    if res:
        res.payload = pkt.payload
        res.session = pkt.session
        res.seq = pkt.seq
        res.marker = pkt.marker
        res.crc = pkt.crc
        res.compressed = pkt.compressed
    else:
        return pkt

    return res

def queue_msg(type, data=None):
    return {'time':datetime.now(), 'type':type, 'data':data}

def reliable_send(io, pkt, dest):
    data = pkt.dump()

    done = False
    timeout = True
    timeouts_count = 0
    while not done:
        if timeout:
            io.sock.sendto(data, dest)

        timeout = False
        rl, wl, xl = io.select ([io.sock], [], [], 1)
        if not rl:
            timeout = True
            timeouts_count += 1
            if timeouts_count > 5:
                return False
            else:
                continue

        ans, dest = io.sock.recvfrom(1500)
        if ans[0] != 'K':
            pass
        elif int(ans.split('-')[1]) != pkt.seq:
            pass
        else:
            done = True

    return True
